package common;

import java.io.*;
import java.net.*;
import java.util.ArrayList;

/**
 * La classe si occupa di gestire il trsferimento di stringhe, attrverso socket, tra due host. 
 * Utilizza due Buffer(pila FIFO) e mette e disposizione metodi per cercare, e recuperare string specifiche
 * 
 * @author Jacopo Ferrigno
 *
 */
//TODO controllare l'accesso asincrono sui file, usare lock!
public class BufferIO extends Thread {
	//Timer per gestione timeout
	private Timer timer;
	//Timeout della connessione = 3 minuti
	private int waitTime=16*60*1000;

	//Parametri di connessione
	private Socket socket;
	private BufferedReader inputReader;
	private BufferedWriter outputWriter;
	private String host="localhost";
	private int port=2000;
	//Buffer per in e out
	private ArrayList<String> Bufferinput;
	private ArrayList<String> Bufferoutput;
	//Variabile sentinella per chiudere la connessione
	private boolean close=false;


	/**
	 * Crea un BufferIO collegato all'host e porta specificate
	 * 
	 * @param host indirizzo dell'host da raggiungere
	 * @param port porta di destinazione sul'host
	 * @throws IOException se sorge qualche problema nella creazione del socket
	 * @throws UnknownHostException se l'host non esiste 
	 * 			o non puo' essere risolto nel corrispettivo indirizzo ip
	 */
	public BufferIO(String host, int port) throws UnknownHostException, IOException{
		super("BuffeIO");
		this.setDaemon(true);
		//salva host e porta
		this.host=host;
		this.port=port;

		//Crea socket e ottine gli Stream reader e Writer associati
		this.socket=new Socket(this.host,this.port);
		this.inputReader=new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
		this.outputWriter=new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream()));
		//Crea i buffer in e out
		this.Bufferinput=new ArrayList<String>();
		this.Bufferoutput=new ArrayList<String>();
		//Crea un timer
		this.timer=new Timer(this.waitTime);
		//this.start();
	}

	/**
	 * Crea un BufferIO utilizzando come canale di collegamento il sochet passato a parmetro
	 * e impostanto un timeout di connessione
	 * @param socket il socket da usare come canale di connessione
	 * @param timeOut timeout di connessione
	 * @throws IOException se sorge qualche problema nella creazione degli StreaHandler
	 */
	public BufferIO(Socket socket, int timeOut) throws IOException{
		super("BuffeIO");
		this.setDaemon(true);
		this.socket=socket;
		this.host=socket.getInetAddress().getHostAddress();
		this.port=socket.getLocalPort();
		this.waitTime=timeOut;

		this.inputReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
		this.outputWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

		this.Bufferinput=new ArrayList<String>();
		this.Bufferoutput=new ArrayList<String>();
		//Crea un timer
		this.timer=new Timer(this.waitTime);
		//this.start();
	}	
	/**
	 * Crea un BufferIO utilizzando come canale di collegamento il sochet passato a parmetro
	 * @param socket il socket da usare come canale di connessione
	 * @throws IOException se sorge qualche problema nella creazione degli StreaHandler
	 */
	public BufferIO(Socket socket) throws  IOException{
		super("BuffeIO");
		this.setDaemon(true);
		this.socket=socket;

		this.host=socket.getInetAddress().getHostAddress();
		this.port=socket.getLocalPort();

		this.inputReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
		this.outputWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

		this.Bufferinput=new ArrayList<String>();
		this.Bufferoutput=new ArrayList<String>();
		//Crea un timer
		this.timer=new Timer(this.waitTime);
		//this.start();
	}

	/**
	 * Scrive la stringa passata a parametro sul buffer in uscita
	 * 
	 * @param message stringa da spedire
	 * @return 0 se e' andata a buon fine, -1 altrimenti
	 */
	public int write(String message){
		//Se riesce ad aggiundere il messaggio ritorna 0

		synchronized (this.Bufferoutput) {
			if(this.Bufferoutput.add(message)){
				return 0;
			}
		}
	
		return -1;

	}

	/**
	 * Ritorna la prima stringa nel buffer(pila FIFO)
	 * 
	 * 
	 * @return il primo messaggio nel buffer, null se e' vuoto.
	 */
	public String read(){
		String message=null;

		synchronized (this.Bufferinput) {
			if(!this.Bufferinput.isEmpty()){
				//Ottieni il primo messaggio in buffer
				message=this.Bufferinput.get(0);
				//Cancella il primo messaggio in buffer che dovrebbe essere stato letto.
				this.Bufferinput.remove(0);

			}
		}

		return message;
	}
	
	/**
	 * Avvia la procedura di chiusura del thread
	 * 
	 */

	public void close(){
		this.close=true;


	}


	/**
	 * Cerca se una data sottostringa e' presente nel buffer in entrata
	 * 
	 * @param la Stringa da cercare
	 * @return la prima posizione all'interno dell buffer in entrata se trovata, -1 altrimenti
	 */
	public synchronized int find(String search){
		int index=-1;

		synchronized (this.Bufferinput){
			for(int i=0; i< this.Bufferinput.size(); i++){
				if(this.Bufferinput.get(i).contains(search)){
					index=i;
					return index;
				}
			}
		}


		return index;


	}

	/**
	 * Ritorna la stringa alla posizione specificata nella pila, come indice
	 * 
	 * @param index indice della stringa interessata
	 * @return la stringa che si trova alla posizione specificata nell'indice
	 * @throws ArrayIndexOutOfBoundsException se l'indice e' fuori dai range della pila
	 */
	public synchronized String messageAt(int index) throws ArrayIndexOutOfBoundsException{
		String message=null;
		//Ottieni il messaggio con incide "index" in buffer
		synchronized (this.Bufferinput) {
			message=this.Bufferinput.get(index);
			//Cancella il primo messaggio in buffer che dovrebbe essere stato letto.
			this.Bufferinput.remove(index);
		}
		
		return message;
	}




	/**
	 * Fa partire il tread che si mette in ascolto in un cilo di
	 * prova a scivere
	 * prova a leggere
	 * 
	 * 
	 */	
	public void run(){

		this.timer.start();
		while(!toClose()){

			//Se il timer si e' esaurito, prova ancora una volta e poi chiudi tutto.
			if(this.timer.outOfTime())
				this.close();
			/*Variabile boleane per controllare se la spedizione 
			 * e'andata a buon fine e quindi posso cansellare il messaggio
				try il send
				se catch sent=false
				finally se sent=true cancella il messaggio,
				altrimenti no. 
			 */
			synchronized (this.Bufferoutput) {
				boolean sent=true;
				if(!this.Bufferoutput.isEmpty()){
					try {
						String message=this.Bufferoutput.get(0);
					if(message ==null)System.out.println("FUCK!");
						
						this.outputWriter.write(message);
						this.outputWriter.newLine();
						this.outputWriter.flush();

					} catch (IOException e) { sent=false;	}
					finally{
						if(sent)
							this.Bufferoutput.remove(0);

					}

				}
			}
			synchronized (this.Bufferinput) {
				boolean isready=false;
				try {isready = this.inputReader.ready();} 
				catch (IOException e) {isready=false;}
				if(isready){

					try {
						this.Bufferinput.add(this.inputReader.readLine());
					} catch (IOException e) {e.printStackTrace();}


					this.timer.reset();
				}
			}





		}

		closeAll();

	}

	/**
	 * Controlla se il buffer in entrata sia vuoto o meno
	 * 
	 * @return true se e' vuoto, false altrimenti.
	 */
	
	public boolean isEmpty(){
		synchronized (this.Bufferinput) {
			return this.Bufferinput.isEmpty();
		}
		


	}

	private boolean toClose(){
		return this.close;
	}

	private void closeAll(){
		try{
			this.inputReader.close();
			this.outputWriter.close();
			this.socket.close();
			this.timer.close();
		}
		catch(Exception e){e.printStackTrace();}
	}

}


